import { CDPSession, Page } from "puppeteer";
import { Recorder } from "./Recorder";
import child_process from "child_process";
import * as fs from 'fs';
import { delay } from "./utils";
import * as os from 'os';
import { console } from "inspector";

class RecordingError extends Error {
    constructor(message: string, public readonly cause?: unknown) {
        super(message);
        this.name = 'RecordingError';
    }
}

type Frame = {
    data: Buffer;
    time: number;
}

export class VideoRecorder extends Recorder {
    private ffmpeg: child_process.ChildProcess | null = null;
    private cdpClient: CDPSession | null = null;
    public  outputPath: string;
    public startTime: number = 0;
    private readonly targetFPS = 25;
    private isRecording = false;
    private frameCount = 0;
    private frames: Frame[] = [];
    private timeStep: number;
    private duration = 0; 
    private lastFrameTime: number = 0;
    private readonly MAX_WAIT_TIME = 5000; // 等待新帧的最大时间（毫秒）

    constructor(private page: Page, outputPath = `output_${Date.now()}.mp4`) {
        super()
        this.outputPath = outputPath;
        this.timeStep = 1000 / this.targetFPS;
    }

    private initFFmpeg(): child_process.ChildProcess {
        this.cleanupExistingFile();
        
        function getEncoderConfig(): string[] {
            const platform = os.platform();
            
            if (platform === 'darwin') {
                return [
                    '-c:v', 'h264_videotoolbox',
                    '-b:v', '5000k',
                    '-tag:v', 'avc1',
                    '-v', 'verbose'  // 添加详细日志
                ];
            }
            if (platform === 'win32') {
                return [
                    '-c:v', 'h264_qsv',
                    '-global_quality', '23',
                    '-preset', 'veryfast'
                ];
            } else if (platform === 'linux') {
                return [
                    '-c:v', 'h264_vaapi',
                    '-qp', '23',
                    '-quality', 'speed'
                ];
            } else {
                return [
                    '-c:v', 'libx264',
                    '-preset', 'ultrafast',
                    '-crf', '23'
                ];
            }
        }
        
        const commonParams = [
            '-f', 'image2pipe',
            '-framerate', `${this.targetFPS}`,
            '-i', '-',
            ...getEncoderConfig(),
            '-r', `${this.targetFPS}`,
            '-pix_fmt', 'yuv420p',
            '-profile:v', 'main',
            '-movflags', '+faststart',
            '-vf', 'scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2',
            '-y',
            this.outputPath
        ];

        console.log('FFmpeg command:', ['ffmpeg', ...commonParams].join(' ')); // 输出完整命令
        const ffmpeg = child_process.spawn("ffmpeg", commonParams);

        this.setupFFmpegEventHandlers(ffmpeg);
        return ffmpeg;
    }

    private cleanupExistingFile(): void {
        try {
            if (fs.existsSync(this.outputPath)) {
                fs.unlinkSync(this.outputPath);
            }
        } catch (error) {
            console.error(`Failed to clean up existing file: ${error}`);
        }
    }

    private setupFFmpegEventHandlers(ffmpeg: child_process.ChildProcess): void {
        if (!ffmpeg.stderr) return;
        
        let logCounter = 0;
        ffmpeg.stderr.on('data', (data: Buffer) => {
            const message = data.toString();
            // 输出所有包含 videotoolbox 的信息
            if (++logCounter % 100 === 0 || logCounter === 1) {
                    console.debug('FFmpeg:', message);
                }
            
        });
        ffmpeg.on('error', (error: Error) => {
            console.error('FFmpeg process error:', error.message);
        });

        ffmpeg.on('exit', (code: number | null, signal: NodeJS.Signals | null) => {
            this.handleFFmpegExit(code, signal);
        });
    }

    private handleFFmpegExit(code: number | null, signal: NodeJS.Signals | null): void {
        if (code !== 0) {
            console.error(`FFmpeg exited with code ${code} and signal ${signal}`);
        } else {
            console.log(`FFmpeg successfully finished. Total frames processed: ${this.frameCount}`);
            this.checkOutputFile();
        }
    }

    private checkOutputFile(): void {
        try {
            const stats = fs.statSync(this.outputPath);
            console.log(`Output file size: ${stats.size} bytes`);
            if (stats.size === 0) {
                console.error('Warning: Output file is empty!');
            }
        } catch (error) {
            console.error('Error checking output file:', error);
        }
    }
  
    /**
     * 将捕获的帧写入到FFmpeg进程，确保音画同步
     * 即使在页面刷新等情况导致部分帧缺失，也能保证后续帧的时间准确性
     */
    private async writeFrameToFFmpeg(): Promise<void> {      
        try {
            let waitStartTime = Date.now();
            
            while (this.isRecording) {
                // 帧缓冲区为空时等待新帧
                if (!this.frames.length) {
                    // 检查是否超过最大等待时间，可能是页面刷新或其他问题
                    if (Date.now() - waitStartTime > this.MAX_WAIT_TIME) {
                        console.warn('No frames received for 5 seconds, possible page reload or issue');
                        // 尝试重启 screencast
                        await this.restartScreencast();
                        waitStartTime = Date.now(); // 重置等待时间
                    }
                    await delay(1000/30); // 短暂等待新帧到来
                    continue;
                }
                
                // 收到新帧，重置等待时间
                waitStartTime = Date.now();
                
                const frame = this.frames[0];
                // 计算当前帧相对于录制开始时间的目标时间点
                const targetDuration = frame.time - this.startTime;
                
                // 如果当前视频持续时间已经超过了这一帧应该出现的时间
                // 说明这是一个"过时"的帧，直接丢弃
                if (this.duration > targetDuration) {
                    this.frames.shift(); // 移除过时帧
                    continue;
                }
                
                // 写入当前帧到FFmpeg
                await this.writeFrameBuffer(frame.data);
                this.frameCount++;
                this.lastFrameTime = frame.time;
                
                // 按固定时间步长递增视频持续时间
                // 这控制了输出视频的帧率，确保平滑播放
                this.duration += this.timeStep;
                
                // 如果递增后的持续时间达到或超过目标时间，移除已处理的帧
                if (this.duration >= targetDuration) {
                    this.frames.shift();
                }
            }
        } catch (error) {
            console.error('Error in writeFrameToFFmpeg:', error);
            if (this.isRecording) {
                // 尝试恢复录制
                console.log('Attempting to recover from error...');
                setTimeout(() => this.writeFrameToFFmpeg(), 1000);
            }
        }        
    }

    private writeFrameBuffer(buffer: Buffer): Promise<boolean> {
        return new Promise((resolve, reject) => {
            this?.ffmpeg?.stdin?.write(buffer, (error) => {
              if (error) {
                reject(error);
              } else {
                resolve(true);
              }
            });
          });
    }

    async start(): Promise<void> {
        if (this.isRecording) {
            await this.stop();
        }
        console.log("Starting video recording...");
        this.resetRecordingState();
        await this.setupPage();
        try {
            this.ffmpeg = this.initFFmpeg();
            await this.initializeCDPClient();
            await this.startScreencast();
            this.writeFrameToFFmpeg();
        } catch (error) {
            this.isRecording = false;
            throw new RecordingError(`Failed to start recording: ${error}`);
        }
    }

    private resetRecordingState(): void {
        this.frames = [];
        this.duration = 0;
        this.startTime = 0;
        this.frameCount = 0;
        this.isRecording = true;
        this.lastFrameTime = 0;
    }

    private async setupPage(): Promise<void> {
        // 设置页面始终可见
        await this.page.evaluateOnNewDocument(() => {
            Object.defineProperty(document, 'hidden', { value: false });
            Object.defineProperty(document, 'visibilityState', { value: 'visible' });
        });
        
        // 添加页面刷新事件监听器
        this.page.on('load', async () => {
            if (this.isRecording) {
                console.log('Page reloaded, restarting screencast...');
                await this.restartScreencast();
            }
        });
    }

    private async initializeCDPClient(retries = 3): Promise<void> {      
        for (let i = 0; i <= retries; i++) {
            try {
                this.cdpClient = await this.page.createCDPSession();
                return;
            } catch (error) {
                if (i === retries) {
                    throw new RecordingError(`Failed to create CDP session after ${retries} attempts: ${error}`);
                }
                console.warn(`CDP session creation failed (attempt ${i+1}/${retries+1}): ${error}`);
                await delay(500); // 等待500ms后重试
            }
        }    
    }

    private async startScreencast(): Promise<void> {
        if (!this.cdpClient) return;

        try {
            await this.cdpClient.send('Page.stopScreencast').catch(err => {
                console.warn('Error stopping previous screencast (may be expected):', err);
            });
            
            await this.cdpClient.send('Page.startScreencast', {
                format: 'jpeg',
                quality: 100,
                maxWidth: 1920,
                maxHeight: 1080,
                everyNthFrame: 1
            });
            
            this.cdpClient.on('Page.screencastFrame', this.handleScreencastFrame.bind(this));
            console.log('Screencast started successfully');
        } catch (error) {
            console.error('Failed to start screencast:', error);
            throw new RecordingError('Failed to start screencast', error);
        }
    }

    /**
     * 重启screencast，用于页面刷新后恢复录制
     */
    private async restartScreencast(): Promise<void> {
        try {
            console.log('Attempting to restart screencast...');
            
            // 如果有旧的CDP客户端，尝试停止并分离
            if (this.cdpClient) {
                try {
                    await this.cdpClient.send('Page.stopScreencast').catch(err => {
                        console.warn('Failed to stop previous screencast:', err);
                    });
                    await this.cdpClient.detach().catch(err => {
                        console.warn('Failed to detach previous CDP client:', err);
                    });
                } catch (error) {
                    console.warn('Error handling previous CDP client:', error);
                }
                this.cdpClient = null;
            }
            
            // 重新初始化CDP客户端并启动screencast
            await this.initializeCDPClient();
            await this.startScreencast();
            console.log('Screencast restarted successfully after page reload');
            
            // 如果之前已有帧，则通过比较最后一帧时间和当前时间来填补可能的时间间隙
            if (this.lastFrameTime > 0) {
                const currentTime = Date.now();
                const timeDiff = currentTime - this.lastFrameTime;
                console.log(`Time gap between last frame and restart: ${timeDiff}ms`);
            }
        } catch (error) {
            console.error('Failed to restart screencast:', error);
        }
    }

    private async handleScreencastFrame(frame: { data: string; sessionId: number }): Promise<void> {
        try {
            if (!this.startTime) {
                this.startTime = Date.now();               
            }

            this.frames.push({
                data: Buffer.from(frame.data, 'base64'),
                time: Date.now()
            });

            if (this.isRecording && this.cdpClient) {
                await this.cdpClient.send('Page.screencastFrameAck', { sessionId: frame.sessionId });
            }
        } catch (error) {
            console.error('Frame processing error:', error);
            if (error instanceof Error && error.message.includes('write after end')) {
                await this.stop();
            } else if (error instanceof Error && error.message.includes('Target closed')) {
                // 页面可能刷新或关闭，尝试重启screencast
                console.warn('Target closed detected, attempting to restart screencast...');
                await this.restartScreencast();
            }
        }
    }

    async stop(): Promise<void> {
        if (!this.isRecording) {
            return;
        }        
        this.isRecording = false;
        console.log('Stopping recording...');
        try {
            // 检查页面是否仍然可用
            if (this.page && !this.page.isClosed()) {
                await this.stopScreencast();
            } else {
                console.log('Page is already closed or not available');
                this.cdpClient = null; // 直接清理 CDP 客户端
            }
            await this.finalizeFFmpeg();
        } catch (error) {
            console.error('Error during stop:', error);
            // 确保即使出错也清理 CDP 客户端
            this.cdpClient = null;
            throw new RecordingError('Failed to stop recording', error);
        }
    }

    private async stopScreencast(): Promise<void> {
        if (this.cdpClient && await this.isCDPClientConnected()) {
            try {
                await this.cdpClient.send('Page.stopScreencast').catch(err => {
                    console.warn('Failed to stop screencast:', err);
                });
                console.log("Screencast stopped");
                await this.cdpClient.detach().catch(err => {
                    console.warn('Failed to detach CDP client:', err);
                });
            } catch (error) {
                console.warn('CDP client stop screencast failed:', error);
            } finally {
                this.cdpClient = null;
            }
        }
    }
    
    private async finalizeFFmpeg(): Promise<void> {
        if (!this.ffmpeg) return;
        return new Promise<void>((resolve, reject) => {
            this.ffmpeg!.stdin?.end();
            this.ffmpeg!.on('exit', (code: number | null) => {
                if (code === 0) {
                    console.log(`Recording completed. Total frames: ${this.frameCount}`);
                    resolve();
                } else {
                    reject(new RecordingError(`FFmpeg exited with code ${code}`));
                }
            });
        });
    }

    private async isCDPClientConnected(): Promise<boolean> {
        try {
            if (!this.cdpClient) return false;
            await this.cdpClient.send('Browser.getVersion');
            return true;
        } catch {
            return false;
        }
    }
}